/*
 * Copyright 2015 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.drools.workbench.models.commons.backend.rule;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.apache.commons.lang3.math.NumberUtils;
import org.drools.core.util.DateUtils;
import org.drools.workbench.models.datamodel.rule.ActionCallMethod;
import org.drools.workbench.models.datamodel.rule.ActionFieldFunction;
import org.drools.workbench.models.datamodel.rule.FieldNatureType;
import org.drools.workbench.models.datamodel.rule.RuleModel;
import org.kie.soup.project.datamodel.oracle.DataType;
import org.kie.soup.project.datamodel.oracle.MethodInfo;
import org.kie.soup.project.datamodel.oracle.PackageDataModelOracle;

import static org.drools.workbench.models.commons.backend.rule.RuleModelPersistenceHelper.adjustParam;
import static org.drools.workbench.models.commons.backend.rule.RuleModelPersistenceHelper.getMethodInfosForType;
import static org.drools.workbench.models.commons.backend.rule.RuleModelPersistenceHelper.inferDataTypeFromActionValue;
import static org.drools.workbench.models.commons.backend.rule.RuleModelPersistenceHelper.inferFieldNature;
import static org.drools.workbench.models.commons.backend.rule.RuleModelPersistenceHelper.removeNumericSuffix;
import static org.drools.workbench.models.commons.backend.rule.RuleModelPersistenceHelper.unwrapTemplateKey;

public class ActionCallMethodBuilder {

    private RuleModel model;
    private PackageDataModelOracle dmo;
    private boolean isJavaDialect;
    private Map<String, String> boundParams;
    private String methodName;
    private String variable;
    private String[] parameters;
    private int index;

    public ActionCallMethodBuilder(RuleModel model,
                                   PackageDataModelOracle dmo,
                                   boolean isJavaDialect,
                                   Map<String, String> boundParams) {
        this.model = model;
        this.dmo = dmo;
        this.isJavaDialect = isJavaDialect;
        this.boundParams = boundParams;
    }

    //ActionCallMethods do not support chained method invocations
    public boolean supports(final String line) {
        final List<String> splits = new ArrayList<String>();
        int depth = 0;
        int textDepth = 0;
        boolean escape = false;
        StringBuffer split = new StringBuffer();
        for (char c : line.toCharArray()) {
            if (depth == 0 && c == '.') {
                splits.add(split.toString());
                split = new StringBuffer();
                depth = 0;
                textDepth = 0;
                escape = false;
                continue;
            } else if (c == '\\') {
                escape = true;
                split.append(c);
                continue;
            } else if (textDepth == 0 && c == '"') {
                textDepth++;
            } else if (!escape && textDepth > 0 && c == '"') {
                textDepth--;
            } else if (textDepth == 0 && c == '(') {
                depth++;
            } else if (textDepth == 0 && c == ')') {
                depth--;
            }
            split.append(c);
            escape = false;
        }
        splits.add(split.toString());
        return splits.size() == 2;
    }

    public ActionCallMethod get(final String variable,
                                final String methodName,
                                final String line) {
        this.variable = variable;
        this.methodName = methodName;
        this.parameters = makeParameters(line);

        ActionCallMethod actionCallMethod = new ActionCallMethod();
        actionCallMethod.setMethodName(methodName);
        actionCallMethod.setVariable(variable);
        actionCallMethod.setState(ActionCallMethod.TYPE_DEFINED);

        for (ActionFieldFunction parameter : getActionFieldFunctions()) {
            actionCallMethod.addFieldValue(parameter);
        }

        return actionCallMethod;
    }

    private String[] makeParameters(final String line) {

        final List<String> result = new ArrayList<>();
        final StringBuilder parameterBuilder = new StringBuilder();

        boolean quoteOpen = false;

        for (int i = 0; i < line.length(); i++) {

            if (isActualQuote(line, i)) {
                quoteOpen = !quoteOpen;
            }

            if (line.charAt(i) == ',' && !quoteOpen) {
                result.add(parameterBuilder.toString());
                parameterBuilder.setLength(0); // reset the builder
            } else {
                parameterBuilder.append(line.charAt(i));
            }
        }

        if (parameterBuilder.length() > 0) {
            result.add(parameterBuilder.toString());
        }

        return result.toArray(new String[result.size()]);
    }

    private boolean isActualQuote(final String line, final int i) {
        final boolean isEscaped = (i > 0 && line.charAt(i - 1) == '\\');
        return !isEscaped && line.charAt(i) == '"';
    }

    private List<ActionFieldFunction> getActionFieldFunctions() {

        List<ActionFieldFunction> actionFieldFunctions = new ArrayList<ActionFieldFunction>();

        this.index = 0;
        for (String param : parameters) {
            param = param.trim();
            if (param.length() == 0) {
                continue;
            }

            actionFieldFunctions.add(getActionFieldFunction(param,
                                                            getDataType(param)));
        }
        return actionFieldFunctions;
    }

    private ActionFieldFunction getActionFieldFunction(String param,
                                                       String dataType) {
        param = removeNumericSuffix(param,
                                    dataType);
        final int fieldNature = inferFieldNature(dataType,
                                                 param,
                                                 boundParams,
                                                 isJavaDialect);

        //If the field is a formula don't adjust the param value
        String paramValue = param;
        switch (fieldNature) {
            case FieldNatureType.TYPE_FORMULA:
                break;
            case FieldNatureType.TYPE_VARIABLE:
                break;
            case FieldNatureType.TYPE_TEMPLATE:
                paramValue = unwrapTemplateKey(param);
                break;
            default:
                paramValue = adjustParam(dataType,
                                         param,
                                         isJavaDialect);
        }
        ActionFieldFunction actionField = new ActionFieldFunction(methodName,
                                                                  paramValue,
                                                                  dataType);
        actionField.setNature(fieldNature);
        return actionField;
    }

    private String getDataType(String param) {
        String dataType;

        MethodInfo methodInfo = getMethodInfo();

        if (methodInfo != null) {
            dataType = methodInfo.getParams().get(index++);
        } else {
            dataType = boundParams.get(param);
        }
        if (dataType == null) {
            dataType = inferDataTypeFromActionValue(param,
                                                    boundParams,
                                                    isJavaDialect);
        }
        return dataType;
    }

    private MethodInfo getMethodInfo() {
        String variableType = boundParams.get(variable);
        if (variableType != null) {
            List<MethodInfo> methods = getMethodInfosForType(model,
                                                             dmo,
                                                             variableType);
            if (methods != null) {

                ArrayList<MethodInfo> methodInfos = getMethodInfos(methodName,
                                                                   methods);

                if (methodInfos.size() > 1) {
                    // Now if there were more than one method with the same name
                    // we need to start figuring out what is the correct one.
                    for (MethodInfo methodInfo : methodInfos) {
                        if (compareParameters(methodInfo.getParams())) {
                            return methodInfo;
                        }
                    }
                } else if (!methodInfos.isEmpty()) {
                    // Not perfect, but works on most cases.
                    // There is no check if the parameter types match.
                    return methodInfos.get(0);
                }
            }
        }

        return null;
    }

    private ArrayList<MethodInfo> getMethodInfos(String methodName,
                                                 List<MethodInfo> methods) {
        ArrayList<MethodInfo> result = new ArrayList<MethodInfo>();
        for (MethodInfo method : methods) {
            if (method.getName().equals(methodName)) {
                result.add(method);
            }
        }
        return result;
    }

    private boolean compareParameters(List<String> methodParams) {
        if (methodParams.size() != parameters.length) {
            return false;
        } else {
            for (int index = 0; index < methodParams.size(); index++) {
                final String methodParamDataType = methodParams.get(index);
                final String paramDataType = assertParamDataType(methodParamDataType,
                                                                 parameters[index].trim());
                if (!methodParamDataType.equals(paramDataType)) {
                    return false;
                }
            }
            return true;
        }
    }

    private String assertParamDataType(final String methodParamDataType,
                                       final String paramValue) {
        if (boundParams.containsKey(paramValue)) {
            //If the parameter is a bound variable use the MethodInfo data-type
            return methodParamDataType;
        } else {
            //Otherwise try coercing the parameter value into the method data-type until a match is found
            if (DataType.TYPE_BOOLEAN.equals(methodParamDataType)) {
                if (Boolean.TRUE.equals(Boolean.parseBoolean(paramValue)) || Boolean.FALSE.equals(Boolean.parseBoolean(paramValue))) {
                    return methodParamDataType;
                }
                return null;
            } else if (DataType.TYPE_DATE.equals(methodParamDataType)) {
                try {
                    new SimpleDateFormat(DateUtils.getDateFormatMask(),
                                         Locale.ENGLISH).parse(adjustParam(methodParamDataType,
                                                                           paramValue,
                                                                           isJavaDialect));
                    return methodParamDataType;
                } catch (ParseException e) {
                    return null;
                }
            } else if (DataType.TYPE_STRING.equals(methodParamDataType)) {
                if (paramValue.startsWith("\"")) {
                    return methodParamDataType;
                }
            } else if (DataType.TYPE_NUMERIC.equals(methodParamDataType)) {
                if (!NumberUtils.isNumber(paramValue)) {
                    return methodParamDataType;
                }
            } else if (DataType.TYPE_NUMERIC_BIGDECIMAL.equals(methodParamDataType)) {
                try {
                    new BigDecimal(adjustParam(methodParamDataType,
                                               paramValue,
                                               isJavaDialect));
                    return methodParamDataType;
                } catch (NumberFormatException e) {
                    return null;
                }
            } else if (DataType.TYPE_NUMERIC_BIGINTEGER.equals(methodParamDataType)) {
                try {
                    new BigInteger(adjustParam(methodParamDataType,
                                               paramValue,
                                               isJavaDialect));
                    return methodParamDataType;
                } catch (NumberFormatException e) {
                    return null;
                }
            } else if (DataType.TYPE_NUMERIC_BYTE.equals(methodParamDataType)) {
                try {
                    Byte.parseByte(paramValue);
                    return methodParamDataType;
                } catch (NumberFormatException e) {
                    return null;
                }
            } else if (DataType.TYPE_NUMERIC_DOUBLE.equals(methodParamDataType)) {
                try {
                    Double.parseDouble(paramValue);
                    return methodParamDataType;
                } catch (NumberFormatException e) {
                    return null;
                }
            } else if (DataType.TYPE_NUMERIC_FLOAT.equals(methodParamDataType)) {
                try {
                    Float.parseFloat(paramValue);
                    return methodParamDataType;
                } catch (NumberFormatException e) {
                    return null;
                }
            } else if (DataType.TYPE_NUMERIC_INTEGER.equals(methodParamDataType)) {
                try {
                    Integer.parseInt(paramValue);
                    return methodParamDataType;
                } catch (NumberFormatException e) {
                    return null;
                }
            } else if (DataType.TYPE_NUMERIC_LONG.equals(methodParamDataType)) {
                try {
                    Long.parseLong(paramValue);
                    return methodParamDataType;
                } catch (NumberFormatException e) {
                    return null;
                }
            } else if (DataType.TYPE_NUMERIC_SHORT.equals(methodParamDataType)) {
                try {
                    Short.parseShort(paramValue);
                    return methodParamDataType;
                } catch (NumberFormatException e) {
                    return null;
                }
            }

            return null;
        }
    }
}
